Cette vignette explique les manières habituelles d’utiliser la
variable .SD dans vos analyses de data.table .
C’est une adaptation ce cette réponse
donnée sur StackOverflow.
.SD?Au sens large, .SD est simplement un raccourci pour
capturer une variable qui apparait fréquemment dans le contexte de
l’analyse de données. Il faut comprendre S pour
Subset, Selfsame, ou Self-reference et
D pour Donnée. Ce qui donne, .SD qui dans
sa forme la plus basique est une référence réflexive de la
data.table elle-même – comme nous le verrons dans les
exemples ci-dessous, ceci est particulièrement utile pour chaîner
ensemble les “requêtes” (extractions/sous-ensembles/etc… en utilisant
[). E particulier cela signifie aussi que
.SD est lui-même une data.table (avec
la mise en garde qu’il ne peut être assigné avec :=).
L’utilisation la plus simple de .SD est l’extraction de
colonnes (c’est à dire si .SDcols est utilisé); parce que
cette version est beaucoup plus facile à comprendre, nous l’aborderons
en priorité ci-dessous. L’interprétation de .SD dans une
seconde étape, en groupant les scénarii (par exemple quand
by = ou keyby = sont spécifiés) est un peu
différent conceptuellement (bien qu’au niveau du noyau ce soit la même
chose car apès tout, uneopération non groupée est un cas aux limites de
groupement avec uniquement un seul groupe).
Pour rendre cela un peu plus concret, plutôt que de modifier les
données, chargeons quelques ensembles de données concernant le baseball
à partir de la base de
données Lahman. Dans R typiquement, nous aurions simplement chargé
ces ensembles de données du package R Lahman; dans cette
vignette, nous les avons préchargés à la place, directement à partir de
la page GitHub du package.
load('Teams.RData')
setDT(Teams)
Teams
# Warning: The packages `ellipsis` (>= 0.3.2) and `vctrs` (>= 0.3.8) are required
# as of rlang 1.0.0.
load('Pitching.RData')
setDT(Pitching)
Pitching
Les lecteurs connaissant le jargon du baseball devraient trouver le
contenu des tableaux familier ; Teams enregistre certaines
statistiques pour une équipe et une année donnée, alors que
Pitching enregistre les statistiques pour un lanceur et une
année donnée. Veuillez lire la documentation et explorer
un peu les données avant d’aller plus loin afin de vous familiariser
avec leur structure.
.SD sur des données non groupéesPour illustrer ce que l’on entend par nature réflexive de
.SD, considérons son utilisation la plus banale :
Pitching[ , .SD]
C’est à dire que Pitching[ , .SD] a simplement renvoyé
la table complète, et c’est une manière exagérément verbeuse d’écrire
Pitching ou Pitching[]:
identical(Pitching, Pitching[ , .SD])
# [1] TRUE
En terme de sous-groupe, .SD est un sous-groupe des
données, le plus évident (c’est l’ensemble lui-même).
.SDcolsLa première façon d’impacter ce que représente .SD c’est
de limiter les colonnes contenues dans .SD en
utilisant l’argument .SDcols dans [ :
# W: Wins; L: Losses; G: Games
Pitching[ , .SD, .SDcols = c('W', 'L', 'G')]
Ceci ne sert que d’illustration et était très ennuyeux. En plus
d’accepter un vecteur de caractères .SDcols accepte
également :
is.character pour filtrer les
colonnespatterns() pour filtrer les
noms des colonnes* avec une expression régulière*voir ?patterns pour davantage de détails
Cette simple utilisation permet une large variété d’opérations avantageuses ou équivalentes de manipulation des données :
La conversion du type de colonne est une réalité en gestion des
données. Bien que fwrite
a récemment gagné la possibilité de déclarer en amont la classe de
chaque colonne, chaque ensemble de données n’est pas forcément issu
d’un fread (comme dans cette vignette) et les conversions
alternatives parmi les types character,
factor, et numeric sont courantes. Nous
pouvons utiliser .SD et .SDcols pour convertir
par lots des groupes de colonnes vers un type commun.
Remarquons que les colonnes suivantes sont rangées en tant que
character dans l’ensemble de données Teams,
mais qu’elles pourraient avantageusement être rangées comme
factor :
# teamIDBR: Team ID used by Baseball Reference website
# teamIDlahman45: Team ID used in Lahman database version 4.5
# teamIDretro: Team ID used by Retrosheet
fkt = c('teamIDBR', 'teamIDlahman45', 'teamIDretro')
# confirm that they're stored as `character`
str(Teams[ , ..fkt])
# Classes 'data.table' and 'data.frame': 2895 obs. of 3 variables:
# $ teamIDBR : chr "BOS" "CHI" "CLE" "KEK" ...
# $ teamIDlahman45: chr "BS1" "CH1" "CL1" "FW1" ...
# $ teamIDretro : chr "BS1" "CH1" "CL1" "FW1" ...
# - attr(*, ".internal.selfref")=<externalptr>
La syntaxe pour convertir ces colonnes en factor est
simple :
Teams[ , names(.SD) := lapply(.SD, factor), .SDcols = patterns('teamID')]
# print out the first column to demonstrate success
head(unique(Teams[[fkt[1L]]]))
# [1] BOS CHI CLE KEK NYU ATH
# 101 Levels: ALT ANA ARI ATH ATL BAL BLA BLN BLU BOS BRA BRG BRO BSN BTT ... WSN
Note :
:= est un opérateur d’assignation pour mettre à jour la
data.table existante sans réaliser de copie. Voir les sémantiques
de référence pour plus d’informations.names(.SD), indique les colonnes à
mettre à jour - dans ce cas il s’agit de tout le .SD.lapply(), boucle sur chaque colonne de
.SD et convertit la colonne en facteur..SDcols pour sélectionner uiquement les
colonnes qui ont pour modèle teamID.A nouveau, l’argument .SDcols est très souple ; nous
avons fourni ci-dessus patterns mais nous aurions pu passer
également fkt ou tout vecteur character de
noms de colonnes. Dans d’autres situations, il est plus pratique de
fournir un vecteur integer de positions des
colonnes ou un vecteur de booléens indiquant pour chaque
colonne s’il faut l’inclure ou l’exclure. Finalement nous utilisons une
fonction pour filtrer les colonnes ce qui est très pratique.
Par exemple nous pourrions faire ceci pour convertir toutes les
colonnes factor en character :
fct_idx = Teams[, which(sapply(.SD, is.factor))] # column numbers to show the class changing
str(Teams[[fct_idx[1L]]])
# Factor w/ 7 levels "AA","AL","FL",..: 4 4 4 4 4 4 4 4 4 4 ...
Teams[ , names(.SD) := lapply(.SD, as.character), .SDcols = is.factor]
str(Teams[[fct_idx[1L]]])
# chr [1:2895] "NA" "NA" "NA" "NA" "NA" "NA" "NA" "NA" "NA" "NA" "NA" "NA" ...
Enfin nous pouvons rechercher la correspondance des colonnes basée
sur des modèles dans .SDcols pour sélectionner toutes les
colonnes qui contiennent team en utilisant
factor :
Teams[ , .SD, .SDcols = patterns('team')]
Teams[ , names(.SD) := lapply(.SD, factor), .SDcols = patterns('team')]
** En plus de ce qui a été dit ci-dessus : utiliser
explicitement* le numéro des colonnes (comme
DT[ , (1) := rnorm(.N)]) n’est pas recommandé et peut
conduire progressivement à obtenir un code corrompu au fil du temps si
la position des colonnes change. Même l’utilisation implicite de numéros
peut être dangereuse si nous ne gardons pas un contrôle intelligent et
strict de l’ordre quand nous créons et utilisons l’index numéroté.
Modifier les spécifications du modèle est une fonctionnalité du noyau
pour l’analyse statistique robuste. Essayons de prédire l’ERA d’un
lanceur (Earned Runs Average, moyenne des tournois gagnés, une mesure de
performance) en utilisant le petit ensemble des covariables disponible
dans la table Pitching. Comment varie la relation
(linéaire) entre W (wins) et ERA en fonction
des autres covariables que l’on inclut dans la spécification ?
Voici une courte description qui évalue la puissance de
.SD explorant cette question :
# this generates a list of the 2^k possible extra variables
# for models of the form ERA ~ G + (...)
extra_var = c('yearID', 'teamID', 'G', 'L')
models = unlist(
lapply(0L:length(extra_var), combn, x = extra_var, simplify = FALSE),
recursive = FALSE
)
# here are 16 visually distinct colors, taken from the list of 20 here:
# https://sashat.me/2017/01/11/list-of-20-simple-distinct-colors/
col16 = c('#e6194b', '#3cb44b', '#ffe119', '#0082c8',
'#f58231', '#911eb4', '#46f0f0', '#f032e6',
'#d2f53c', '#fabebe', '#008080', '#e6beff',
'#aa6e28', '#fffac8', '#800000', '#aaffc3')
par(oma = c(2, 0, 0, 0))
lm_coef = sapply(models, function(rhs) {
# using ERA ~ . and data = .SD, then varying which
# columns are included in .SD allows us to perform this
# iteration over 16 models succinctly.
# coef(.)['W'] extracts the W coefficient from each model fit
Pitching[ , coef(lm(ERA ~ ., data = .SD))['W'], .SDcols = c('W', rhs)]
})
barplot(lm_coef, names.arg = sapply(models, paste, collapse = '/'),
main = 'Coefficient Wins \navec diverses covariables',
col = col16, las = 2L, cex.names = 0.8)
Ajustement du coefficient OLS sur W, diverses spécifications, décrites par les barres de couleur différente.
Le coefficient a toujours le signe attendu (les meilleurs lanceurs ont tendance à avoir plus de victoires et moins de tournois autorisés), mais l’amplitude peut varier substantiellement en fonction de ce qui est contrôlé par ailleurs.
La syntaxe de data.table est belle par sa simplicité et
sa robustesse. La syntaxe x[i] gère de manière souple trois
approches communes du sous-groupement – si i est un vecteur
booléen, x[i] renvoie les lignes de
x qui correspondent aux indices où i vaut
TRUE; si i est une autre
data.table (ou une list), une
jointure droite (join right) est réalisée (dans la forme à
plat, en utilisant les clés de x et
i, sinon, si on = est spécifié, en utilisant
les colonnes qui correspondent); et si i est un caratère,
il est interprété comme raccourci pour x[list(i)], c’est à
dire comme une jointure.
C’est puissant en général, mais perd de sa valeur rapidement si on souhaite réaliser une jointure conditionnelle, où la nature exacte de la relation entre les tables dépend de certaines caractéristiques des lignes dans une ou plusieurs colonnes.
Cet exemple est certes un peu artificiel, mais il illustre l’idée ; voir ici (1, 2) pour plus d’informations.
Le but est d’ajouter une colonne team_performance à la
table Pitching qui enregistre les performances de l’équipe
(rang) du meilleur lanceur de chaque équipe (tel que mesuré par le ERA
le plus faible, parmi les lanceurs ayant au moins 6 jeux
enregistrés).
# to exclude pitchers with exceptional performance in a few games,
# subset first; then define rank of pitchers within their team each year
# (in general, we should put more care into the 'ties.method' of frank)
Pitching[G > 5, rank_in_team := frank(ERA), by = .(teamID, yearID)]
Pitching[rank_in_team == 1, team_performance :=
Teams[.SD, Rank, on = c('teamID', 'yearID')]]
Notez que la syntaxe de x[y] renvoie
nrow(y) values (c’est une jointure droite), c’est pourquoi
.SD se trouve à droite dans Teams[.SD] (parce
que le membre de droite de := dans ce cas nécessite les
valeurs de nrow(Pitching[rank_in_team == 1]) ).
.SD groupéesNous aimerions souvent réaliser une opération sur nos données au
niveau groupe. Si nous indiquons by = (ou
keyby =), le modèle que nous imaginons mentalement pour ce
qui se passe quand data.table traite j est de
considérer que la data.table est constituée de plusieurs
composants sous-data.table, dont chacun correspond à une
seule valeur des variables du by :
Groupement, Image
En cas de groupement, .SD est multiple par nature – il
se réfère à chaque
sous-data.table, *une à la fois* (ou plus précisément, la visibilité de.SDest une sous-data.tableunique). Ceci nous permet d'indiquer précisément une opération à réaliser sur *chaque sous-data.table`*
avant de réassembler et renvoyer le résultat.
C’est utile pour diverses initialisations, les plus communes sont présentées ici :
Essayons d’obtenir la saison la plus récente des données pour chaque équipe des données Lahman. Ceci peut être fait simplement avec :
# the data is already sorted by year; if it weren't
# we could do Teams[order(yearID), .SD[.N], by = teamID]
Teams[ , .SD[.N], by = teamID]
Rappelez-vous que .SD est lui-même une
data.table, et que .N se rapporte au nombre
total de lignes dans un groupe (c’est égal à nrow(.SD) à
l’intérieur de chaque groupe), donc .SD[.N] renvoie la
totalité de .SD pour la dernière ligne associée à
chaque teamID.
Une autre version commune de ceci est l’utilisation de
.SD[1L] à la place, pour obtenir la première
observation de chaque groupe, ou .SD[sample(.N, 1L)] pour
renvoyer une ligne aléatoire pour chaque groupe.
Supposons que nous voulions renvoyer la meilleure année pour
chaque équipe, tel que mesuré par leur nombre total de tournois
enregistrés (R; il est facile d’ajuster cela pour s’adapter
à d’autres métriques, bien sûr). Au lieu de prendre un élément
fixe de chaque sous-data.table, nous définissons
maintenant dynamiquement l’indice souhaité ainsi :
Teams[ , .SD[which.max(R)], by = teamID]
Notez que cette approche peut bien sûr être combinée avec
.SDcols pour renvoyer uniquement les portions de
data.table pour chaque .SD (avec la mise en
garde que .SDcols soit initialisé en fonction des
différents sous-ensembles)
N.B.: .SD[1L] est actuellement optimisé par GForce
(voir
aussi), fonctionnalités internes de data.table qui
accélèrent massivement les opérations groupées le plus souvent telles
que sum ou mean – see ?GForce
pour plus de détails et garder un oeil dessus / prise en charge de la
voix pour les demandes de mises à jour de l’amélioration des
fonctionnalités sur ce front : 1, 2, 3, 4, 5, 6
Revenons à la requête ci-dessus à propos des relations entre
ERA et W; supposez que nous espérions que
cette relation soit différente en fonction de l’équipe (c’est à dire que
la pente soit différente pour chaque équipe). Nous pouvons facilement
réexécuter cette régression pour explorer l’hétérogenéité dans cette
relation comme ceci (en notant que les erreurs standard de cette
approche sont généralement incorrectes – la spécification
ERA ~ W*teamID sera meilleurs – cette approche est plus
facile à lire et les coefficients sont OK) :
# Overall coefficient for comparison
overall_coef = Pitching[ , coef(lm(ERA ~ W))['W']]
# use the .N > 20 filter to exclude teams with few observations
Pitching[ , if (.N > 20L) .(w_coef = coef(lm(ERA ~ W))['W']), by = teamID
][ , hist(w_coef, 20L, las = 1L,
xlab = 'Coefficient ajusté sur W',
ylab = 'Nombre d\'équipes', col = 'darkgreen',
main = 'Distribution du niveau des équipes\nCoefficients Win sur ERA')]
abline(v = overall_coef, lty = 2L, col = 'red')
Histogramme décrivant la distribution des coefficients ajustés. La courbe représente à peut près une cloche centrée sur -0,2
Tandis qu’il existe une grande hétérogénéité, la concentration autour de la valeur générale observée reste très distincte.
Tout ceci n’est simplement qu’une brève introduction sur la puissance
de .SD qui facilite la beauté et l’efficacité du code dans
data.table !